{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Fitting kinetic parameters to experimental data\n",
    "Author: Björn Dahlgren.\n",
    "\n",
    "Let us consider the reaction:\n",
    "\n",
    "$$\n",
    "Fe^{3+} + SCN^- \\rightarrow FeSCN^{2+}\n",
    "$$\n",
    "\n",
    "the product is strongly coloured and we have experimental data (from a stopped-flow appartus) of the absorbance as function of time after mixing for several replicates. The experiment was performed at 7 different temperatures and for one temperature, 7 different ionic strengths. For each set of conditions the experiment was re-run 7 times (replicates). In this notebook, we will determine the activation enthalpy and entropy through regressions analysis, we will also look at the ionic strength dependence."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import bz2, codecs, collections, functools, itertools, json\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import chempy\n",
    "import chempy.equilibria\n",
    "from chempy.electrolytes import ionic_strength\n",
    "from chempy.kinetics.arrhenius import fit_arrhenius_equation\n",
    "from chempy.printing import number_to_scientific_latex, as_per_substance_html_table\n",
    "from chempy.properties.water_density_tanaka_2001 import water_density\n",
    "from chempy.units import rescale, to_unitless, default_units as u\n",
    "from chempy.util.regression import least_squares, irls, avg_params, plot_fit, plot_least_squares_fit, plot_avg_params\n",
    "from chempy._solution import QuantityDict\n",
    "%matplotlib inline\n",
    "print(chempy.__version__)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Experimental conditions, the two solutions which were mixed in 1:1 volume ratio in a stopped flow apparatus:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sol1 = QuantityDict(u.mM, {'SCN-': 3*u.mM, 'K+': 3*u.mM, 'Na+': 33*u.mM, 'H+': 50*u.mM, 'ClO4-': (33+50)*u.mM})\n",
    "sol2 = QuantityDict(u.mM, {'Fe+3': 6*u.mM, 'H+': 50*u.mM, 'ClO4-': (3*6+50)*u.mM})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sol = (sol1 + sol2)/2  # 1:1 volume ratio at mixing\n",
    "sol.quantity_name = 'concentration'\n",
    "Ibase = ionic_strength(rescale(sol/water_density(293*u.K, units=u), u.molal))\n",
    "print(Ibase)\n",
    "sol"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ionic_strength_keys = 'abcd'\n",
    "ionic_strengths = dict(zip(ionic_strength_keys, [0, 20, 40, 60]*u.molal*1e-3))\n",
    "temperature_keys = '16.5 18.5 20.5 22.5 24.5'.split()\n",
    "T0C = 273.15*u.K\n",
    "temperatures = {k: T0C + float(k)*u.K for k in temperature_keys}\n",
    "nrep = 7\n",
    "indices = lambda k: (ionic_strength_keys.index(k[0]), temperature_keys.index(k[1]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We will read the data from a preprocessed file:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "transform = np.array([[1e-3, 0], [0, 1e-4]])  # converts 1st col: ms -> s and 2nd col to absorbance\n",
    "_reader = codecs.getreader(\"utf-8\")\n",
    "_dat = {tuple(k): np.dot(np.array(v), transform) for k, v in json.load(_reader(bz2.BZ2File('specdata.json.bz2')))}\n",
    "data = collections.defaultdict(list)\n",
    "for (tI, tT, tR), v in _dat.items():\n",
    "    k = (tI, tT)  # tokens for ionic strength and temperatures\n",
    "    data[k].append(v)\n",
    "assert len(data) == len(ionic_strengths)*len(temperatures) and all(len(serie) == nrep for serie in data.values())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's plot the data:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def mk_subplots(nrows=1, subplots_adjust=True, **kwargs):\n",
    "    fig, axes = plt.subplots(nrows, len(ionic_strengths), figsize=(15,6), **kwargs)\n",
    "    if subplots_adjust:\n",
    "        plt.subplots_adjust(hspace=0.001, wspace=0.001)\n",
    "    return axes\n",
    "\n",
    "def _set_axes_titles_to_ionic_strength(axes, xlim=None, xlabel=None):\n",
    "    for tI, ax in zip(ionic_strength_keys, axes):\n",
    "        ax.set_title(r'$I\\ =\\ %s$' % number_to_scientific_latex(Ibase + ionic_strengths[tI], fmt=3))\n",
    "        if xlabel is not None:\n",
    "            ax.set_xlabel(xlabel)\n",
    "        if xlim is not None:\n",
    "            ax.set_xlim(xlim)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_series(series):\n",
    "    axes = mk_subplots(sharey=True, sharex=True)\n",
    "    colors = 'rgbmk'\n",
    "    for key in itertools.product(ionic_strengths, temperatures):\n",
    "        idx_I, idx_T = indices(key)\n",
    "        for serie in series[key]:\n",
    "            axes[idx_I].plot(serie[:, 0], serie[:, 1], c=colors[idx_T], alpha=0.15)\n",
    "    _set_axes_titles_to_ionic_strength(axes, xlim=[0, 3.4], xlabel='Time / s')\n",
    "    axes[0].set_ylabel('Absorbance')\n",
    "    for c, tT in zip(reversed(colors), reversed(temperature_keys)):\n",
    "        axes[0].plot([], [], c=c, label=tT + ' °C')\n",
    "    axes[0].legend(loc='best')\n",
    "plot_series(data)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We see that one data series is off: 16.5 ℃ and 0.0862 molal. Let's ignore that for now and perform the fitting, let's start with a pseudo-first order assumption (poor but simple):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def fit_pseudo1(serie, ax=None):\n",
    "    plateau = np.mean(serie[2*serie.shape[0]//3:, 1])\n",
    "    y = np.log(np.clip(plateau - serie[:, 1], 1e-6, 1))\n",
    "    x = serie[:, 0]\n",
    "    # irls: Iteratively reweighted least squares\n",
    "    res = irls(x, y, irls.gaussian)\n",
    "    if ax is not None:\n",
    "        plot_least_squares_fit(x, y, res)\n",
    "    return res\n",
    "\n",
    "beta, vcv, info = fit_pseudo1(data['a', '16.5'][0], ax=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def fit_all(series, fit_cb=fit_pseudo1, plot=False):\n",
    "    if plot:\n",
    "        axes = mk_subplots(nrows=len(temperatures), sharex=True, sharey=True)#, subplots_adjust=False)\n",
    "        _set_axes_titles_to_ionic_strength(axes[0, :])\n",
    "    avg = {}\n",
    "    for key in itertools.product(ionic_strengths, temperatures):\n",
    "        idx_I, idx_T = indices(key)\n",
    "        opt_params, cov_params = [], []\n",
    "        for serie in series[key]:\n",
    "            beta, vcv, nfo = fit_cb(serie)\n",
    "            opt_params.append(beta)\n",
    "            cov_params.append(vcv)\n",
    "        ax = axes[idx_T, idx_I] if plot else None\n",
    "        avg[key] = avg_params(opt_params, cov_params)\n",
    "        plot_avg_params(opt_params, cov_params, avg[key], ax=ax, flip=True, nsigma=3)\n",
    "    for tk, ax in zip(temperature_keys, axes[:, 0]):\n",
    "        ax.set_ylabel('T = %s C' % tk)\n",
    "    return avg\n",
    "\n",
    "result_pseudo1 = fit_all(data, plot=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def pseudo_to_k2(v):\n",
    "    unit = 1/sol['Fe+3']/u.second\n",
    "    k_val = -v[0][1]*unit\n",
    "    k_err = v[1][1]*unit\n",
    "    return k_val, k_err\n",
    "\n",
    "k_pseudo1 = {k: pseudo_to_k2(v) for k, v in result_pseudo1.items()}\n",
    "k_pseudo1['a', '16.5']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "k2_unit = 1/u.M/u.s\n",
    "axes = mk_subplots(sharex=True, sharey=True)\n",
    "for idxI, (tI, I) in enumerate(ionic_strengths.items()):\n",
    "    series = np.empty((len(temperatures), 3))\n",
    "    for idxT, (tT, T) in enumerate(temperatures.items()):\n",
    "        kval, kerr = [to_unitless(v, k2_unit) for v in k_pseudo1[tI, tT]]\n",
    "        lnk_err = (np.log(kval + kerr) - np.log(kval - kerr))/2\n",
    "        series[idxT, :] = to_unitless(1/T, 1/u.K), np.log(kval), 1/lnk_err**2\n",
    "    x, y, w = series.T\n",
    "    res = b, vcv, r2 = least_squares(x, y, w)\n",
    "    plot_least_squares_fit(x, y, res, w**-0.5, plot_cb=functools.partial(\n",
    "            plot_fit, ax=axes[idxI], kw_data=dict(ls='None', marker='.'), nsigma=3))\n",
    "    axes[idxI].get_lines()[-1].set_label(r'$E_{\\rm a} = %.5g\\ kJ/mol$' % (-b[1]*8.314511e-3))\n",
    "    axes[idxI].legend()\n",
    "_set_axes_titles_to_ionic_strength(axes)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
